Skip to content

feat: invoice delete v3 endpoint#4654

Merged
borosr merged 2 commits into
mainfrom
feat/v3-invoice-api-delete
Jul 7, 2026
Merged

feat: invoice delete v3 endpoint#4654
borosr merged 2 commits into
mainfrom
feat/v3-invoice-api-delete

Conversation

@borosr

@borosr borosr commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

Adds DELETE /api/v3/openmeter/billing/invoices/{invoiceId} — an unstable/internal/private v3 endpoint to delete a standard billing invoice.

  • New handler api/v3/handlers/billinginvoices/delete.go (DeleteBillingInvoice), wired into handler.go, routes.go, and the generated api.gen.go/openapi.yaml.
  • New TypeSpec operation in api/spec/packages/aip/src/invoices/operations.tsp, with a matching delete method on the generated JS SDK's client.invoices.
  • Extracted validateAPIInvoiceDeleteSupported/validateAPIGenericInvoiceDeleteSupported out of openmeter/billing/httpdriver/invoice.go into a new exported openmeter/billing/invoice_validator.go (ValidateAPIInvoiceDeleteSupported/ValidateAPIGenericInvoiceDeleteSupported), so both the legacy v1 handler and the new v3 handler share the same guard.
  • Only standard invoices can be deleted via this endpoint; gathering invoices and unknown IDs return 404. Invoices containing usage-based charge lines are rejected (usage-based invoice-scope deletion isn't implemented yet).
  • Added TestV3DeleteBillingInvoice (e2e) and a DeleteBillingInvoice client helper covering the full lifecycle: delete a draft standard invoice → 204, delete again → 404, delete a gathering invoice → 404, delete an unknown ID → 404.

Why

The v3 API already supports get/list/update for billing invoices; delete was the remaining gap for basic invoice lifecycle management. The validator was pulled out to a shared, exported location instead of being duplicated, since both the v1 and v3 delete paths need to enforce the same "no usage-based charge lines" guard ahead of side-effectful line-engine cleanup.

How

  • Fixed two bugs found in review before they shipped:
    • The initial handler parsed a JSON request body (ParseBody) that neither the TypeSpec nor the generated SDK ever sends for a DELETE — every real (bodyless) client call would have hit io.EOF and failed with 400. Removed the dead parsing block.
    • The initial handler fetched the invoice via GetInvoiceById without Expand: billing.InvoiceExpandAll, so Lines was never populated and the usage-based-charge-line guard silently iterated zero lines — it would never actually block a disallowed delete. Added the missing Expand.
    • Also dropped an unreachable switch/default branch left over from the case where the handler still supported gathering-invoice deletion (it's now rejected earlier via an explicit type check), and added the missing TypeSpec doc comment for the new operation (the other three invoice operations all have one; this one's SDK README row was blank without it).
  • Added TestV3DeleteBillingInvoice, modeled on TestV3UpdateBillingInvoice: pins the customer to a manual-approval billing profile so the standard invoice stays in draft (deletable) instead of auto-advancing, then exercises the delete/re-delete/gathering/unknown-ID cases end-to-end against a live server.

Outstanding before merge: the TypeSpec doc-comment change hasn't been run through make gen-api yet, so api/v3/openapi.yaml and the generated JS SDK README are still out of sync with operations.tsp. Want me to run that now?

Overview

Fixes #(issue)

Notes for reviewer

Summary by CodeRabbit

  • New Features

    • Added billing invoice deletion across the API, server routes (v3), and SDK, returning 204 No Content on success.
    • Added a new invoice delete operation to the generated SDK surface.
  • Bug Fixes

    • Improved deletion validation to reject unsupported invoice types and prevent deleting invoices with non-deletable usage-based charge lines.
    • Deleting an already-deleted invoice now behaves more consistently with validation rules.
  • Tests

    • Added end-to-end coverage for delete success and common error scenarios.
  • Documentation

    • Updated the generated SDK README to include the new delete invoice action.

@borosr
borosr requested a review from a team as a code owner July 7, 2026 09:34
@borosr borosr self-assigned this Jul 7, 2026
@borosr
borosr requested review from rolosp, tothandras and turip July 7, 2026 09:35
@borosr borosr added kind/feature New feature or request release-note/feature Release note: Exciting New Features labels Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0377a715-d24d-4132-973b-b218402a5ef9

📥 Commits

Reviewing files that changed from the base of the PR and between b6b85f2 and 16bb9d6.

📒 Files selected for processing (2)
  • api/spec/packages/aip-client-javascript/src/funcs/invoices.ts
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts

📝 Walkthrough

Walkthrough

This PR adds delete-invoice support across billing validation, v3 server routing, generated API surfaces, the JavaScript SDK, and end-to-end coverage.

Changes

Delete Invoice Feature

Layer / File(s) Summary
Shared invoice delete validation
openmeter/billing/invoice_validator.go, openmeter/billing/httpdriver/invoice.go, openmeter/billing/httpdriver/invoice_test.go
Adds exported delete-validation functions and switches the HTTP driver and tests to use them.
Delete invoice HTTP handler and routing
api/v3/handlers/billinginvoices/delete.go, api/v3/handlers/billinginvoices/handler.go, api/v3/server/routes.go
Implements DeleteBillingInvoice, adds the handler interface method, and wires the server route.
Generated server interface and OpenAPI spec
api/v3/api.gen.go
Adds the generated DeleteInvoice handler, wrapper, route registration, and swagger spec updates.
TypeSpec and JS SDK delete support
api/spec/packages/aip/src/invoices/operations.tsp, api/spec/packages/aip-client-javascript/src/..., api/spec/packages/aip-client-javascript/README.md
Adds the delete operation to the TypeSpec and generated JS client surface.
End-to-end delete invoice tests
e2e/v3helpers_test.go, e2e/billinginvoices_v3_test.go
Adds a v3 delete helper and end-to-end coverage for delete scenarios.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server
  participant DeleteBillingInvoiceHandler
  participant billing_service as "billing.Service"

  Client->>Server: DELETE /openmeter/billing/invoices/{invoiceId}
  Server->>DeleteBillingInvoiceHandler: DeleteBillingInvoice().With(invoiceId)
  DeleteBillingInvoiceHandler->>billing_service: GetInvoice (expanded)
  billing_service-->>DeleteBillingInvoiceHandler: invoice
  DeleteBillingInvoiceHandler->>DeleteBillingInvoiceHandler: ValidateAPIInvoiceDeleteSupported(invoice)
  DeleteBillingInvoiceHandler->>billing_service: DeleteInvoice(DeletionSource=APIRequest)
  billing_service-->>DeleteBillingInvoiceHandler: result status
  DeleteBillingInvoiceHandler-->>Client: 204 No Content or ValidationError
Loading

Possibly related PRs

Suggested labels: area/billing, release-note/bug-fix

Suggested reviewers: tothandras, rolosp, turip, GAlexIHU

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a v3 invoice delete endpoint.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v3-invoice-api-delete

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a v3 endpoint for deleting billing invoices. The main changes are:

  • A new DELETE /openmeter/billing/invoices/{invoiceId} route.
  • A v3 handler that deletes standard billing invoices.
  • Shared invoice delete validation for v1 and v3 paths.
  • Generated OpenAPI, Go server, and JavaScript SDK updates.
  • End-to-end coverage for delete success and expected error cases.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
api/v3/handlers/billinginvoices/delete.go Adds the v3 invoice delete handler with expanded invoice lookup, standard-invoice filtering, shared validation, service deletion, and empty 204 responses.
openmeter/billing/invoice_validator.go Moves invoice delete validation into the billing package so v1 and v3 delete paths share the same usage-based line guard.
api/spec/packages/aip-client-javascript/src/funcs/invoices.ts Adds the JavaScript SDK delete helper with a bodyless request and void response handling.
api/v3/server/routes.go Connects the generated v3 delete operation to the billing invoices handler.
e2e/billinginvoices_v3_test.go Adds an end-to-end test for deleting a standard invoice and checking repeat, gathering, and unknown invoice cases.

Reviews (2): Last reviewed commit: "fix: regenerate the js client" | Re-trigger Greptile

if err != nil {
return nil, err
}
if existing.Type() != billing.InvoiceTypeStandard {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Issuing Invoices Can Be Deleted

When a standard invoice has already left draft and is in an issuing state such as issuing.syncing or issuing.charge_booking, this handler still calls DeleteInvoice because it only checks the invoice type. The billing state machine permits delete transitions from those issuing states, so this endpoint can return 204 and delete an invoice that the v3 API contract says is no longer deletable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: api/v3/handlers/billinginvoices/delete.go
Line: 47

Comment:
**Issuing Invoices Can Be Deleted**

When a standard invoice has already left draft and is in an issuing state such as `issuing.syncing` or `issuing.charge_booking`, this handler still calls `DeleteInvoice` because it only checks the invoice type. The billing state machine permits delete transitions from those issuing states, so this endpoint can return 204 and delete an invoice that the v3 API contract says is no longer deletable.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not for code to decide.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — the state machine owns those transition rules, so if DeleteInvoice rejects the transition for an issuing-state invoice, the handler will naturally surface that error. Adding a redundant state check here would duplicate logic that already lives in the right place.

Shall I remember this as a rule: "Do not add invoice state guards in v3 handlers; state transition validation belongs to the billing state machine"?

@turip turip left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgfm

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
openmeter/billing/invoice_validator.go (1)

44-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing doc comment on exported function.

ValidateAPIGenericInvoiceDeleteSupported is exported but has no doc comment (unlike ValidateAPIInvoiceDeleteSupported right above it). Since this is now part of the shared public API surface (used by two call sites), a short comment would help future callers understand the usage-based-line guard without diving into the loop body.

📝 Suggested doc comment
+// ValidateAPIGenericInvoiceDeleteSupported blocks deletion of invoices that
+// still have active (non-deleted) usage-based charge lines, since invoice-scope
+// deletion of those lines is not yet implemented.
 func ValidateAPIGenericInvoiceDeleteSupported(invoice GenericInvoice) error {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openmeter/billing/invoice_validator.go` at line 44, Add a short doc comment
for the exported ValidateAPIGenericInvoiceDeleteSupported function, matching the
style of ValidateAPIInvoiceDeleteSupported. The comment should briefly describe
that it validates invoice deletion support based on usage-derived line items and
help callers understand the guard without reading the loop body. Keep the
comment directly above ValidateAPIGenericInvoiceDeleteSupported so the public
API is documented.
e2e/billinginvoices_v3_test.go (1)

1043-1236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding given/when/then intent comments to the subtests.

None of the new subtests here open with a short given/when/then style comment. As per path instructions, "For service and lifecycle subtests, start each non-trivial subtest body with concise intent comments in given/when/then form." Since this test walks through the invoice delete lifecycle (create → advance → delete → repeat delete → gathering delete → unknown-id delete), it's a good fit for that convention.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/billinginvoices_v3_test.go` around lines 1043 - 1236, The new lifecycle
subtests in billinginvoices_v3_test.go should start with concise given/when/then
intent comments. Update each non-trivial t.Run block in this invoice delete
flow—especially the customer setup, billing profile pinning, pending invoice
creation, subscription/plan setup, invoice advancement, delete, repeat delete,
and gathering-invoice delete cases—to add short given/when/then style comments
at the top of the subtest body. Keep the comments aligned with the existing test
intent and use the t.Run names and helpers like
createNewBillingProfileFromDefault, CreatePendingInvoiceLineWithResponse,
CreateSubscription, and DeleteBillingInvoice as anchors.

Source: Path instructions

api/spec/packages/aip/src/invoices/operations.tsp (1)

138-143: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Doc comment misses the usage-based-lines rejection rule.

Per the PR notes, invoices with usage-based charge lines get rejected on delete too, not just non-draft/non-standard ones — worth calling that out here so API consumers aren't surprised by an error they can't predict from the docs alone.

As per path instructions, "The declared API should be accurate, in parity with the actual implementation, and easy to understand for the user."

📝 Suggested doc tweak
   /**
    * Delete a billing invoice.
    *
-   * Only standard invoices in draft status can be deleted. Deleting an invoice will
-   * also delete all associated line items and workflow configuration.
+   * Only standard invoices in draft status can be deleted, and only if they do not
+   * contain usage-based charge lines. Deleting an invoice will also delete all
+   * associated line items and workflow configuration.
    */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/aip/src/invoices/operations.tsp` around lines 138 - 143,
The delete invoice doc comment on the delete operation should mention the
usage-based charge line rejection rule in addition to the existing
draft/standard restriction. Update the comment attached to the delete billing
invoice operation in the operations definition so consumers understand that
invoices containing usage-based lines are also rejected by the delete path,
keeping the documentation aligned with the delete implementation and related
invoice validation logic.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/spec/packages/aip-client-javascript/src/funcs/invoices.ts`:
- Around line 100-111: The path construction in deleteInvoice is happening
before request(), so a missing req.invoiceId can throw outside the Result
wrapper. Move the encodePath('openmeter/billing/invoices/{invoiceId}', ...) call
inside the request() callback in deleteInvoice so any invalid input is captured
consistently like the other invoice methods. Keep the http(client).delete(...)
call within that same request block using the computed path.

---

Nitpick comments:
In `@api/spec/packages/aip/src/invoices/operations.tsp`:
- Around line 138-143: The delete invoice doc comment on the delete operation
should mention the usage-based charge line rejection rule in addition to the
existing draft/standard restriction. Update the comment attached to the delete
billing invoice operation in the operations definition so consumers understand
that invoices containing usage-based lines are also rejected by the delete path,
keeping the documentation aligned with the delete implementation and related
invoice validation logic.

In `@e2e/billinginvoices_v3_test.go`:
- Around line 1043-1236: The new lifecycle subtests in
billinginvoices_v3_test.go should start with concise given/when/then intent
comments. Update each non-trivial t.Run block in this invoice delete
flow—especially the customer setup, billing profile pinning, pending invoice
creation, subscription/plan setup, invoice advancement, delete, repeat delete,
and gathering-invoice delete cases—to add short given/when/then style comments
at the top of the subtest body. Keep the comments aligned with the existing test
intent and use the t.Run names and helpers like
createNewBillingProfileFromDefault, CreatePendingInvoiceLineWithResponse,
CreateSubscription, and DeleteBillingInvoice as anchors.

In `@openmeter/billing/invoice_validator.go`:
- Line 44: Add a short doc comment for the exported
ValidateAPIGenericInvoiceDeleteSupported function, matching the style of
ValidateAPIInvoiceDeleteSupported. The comment should briefly describe that it
validates invoice deletion support based on usage-derived line items and help
callers understand the guard without reading the loop body. Keep the comment
directly above ValidateAPIGenericInvoiceDeleteSupported so the public API is
documented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64b72522-41dc-41df-9c6a-2a391142c544

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf58b9 and b6b85f2.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (15)
  • api/spec/packages/aip-client-javascript/README.md
  • api/spec/packages/aip-client-javascript/src/funcs/invoices.ts
  • api/spec/packages/aip-client-javascript/src/models/operations/invoices.ts
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/sdk/invoices.ts
  • api/spec/packages/aip/src/invoices/operations.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/billinginvoices/delete.go
  • api/v3/handlers/billinginvoices/handler.go
  • api/v3/server/routes.go
  • e2e/billinginvoices_v3_test.go
  • e2e/v3helpers_test.go
  • openmeter/billing/httpdriver/invoice.go
  • openmeter/billing/httpdriver/invoice_test.go
  • openmeter/billing/invoice_validator.go

Comment thread api/spec/packages/aip-client-javascript/src/funcs/invoices.ts
@borosr
borosr enabled auto-merge (squash) July 7, 2026 09:44
@borosr
borosr merged commit 50d7ae9 into main Jul 7, 2026
29 checks passed
@borosr
borosr deleted the feat/v3-invoice-api-delete branch July 7, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature New feature or request release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants